///////////////////////////////////////////////////////////////////////////////
//
//                          IMPORTANT NOTICE
//
// The following open source license statement does not apply to any
// entity in the Exception List published by FMSoft.
//
// For more information, please visit:
//
// https://www.fmsoft.cn/exception-list
//
//////////////////////////////////////////////////////////////////////////////
/*
** This file is a part of mg-demos package.
**
** Copyright (C) 2008 ~ 2019 FMSoft (http://www.fmsoft.cn).
**
** Licensed under the Apache License, Version 2.0 (the "License");
** you may not use this file except in compliance with the License.
** You may obtain a copy of the License at
**
**     http://www.apache.org/licenses/LICENSE-2.0
**
** Unless required by applicable law or agreed to in writing, software
** distributed under the License is distributed on an "AS IS" BASIS,
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
** See the License for the specific language governing permissions and
** limitations under the License.
*/
/*
** softkeywindow.c.
**
** Create date: 2020/7/18
*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#include <assert.h>

#include <minigui/common.h>
#include <minigui/minigui.h>
#include <minigui/gdi.h>
#include <minigui/window.h>

#include "common.h"
#include "softkeywindow.h"
#include "tooltip.h"

#if KBD_ANIMATE
#include "animate/animate.h"
#include "animate/common_animates.h"
#endif

#ifdef DEBUG
#define ENTER() printf(">>>   enter %s\n", __FUNCTION__);
#define LEAVE() printf("<<<   leave %s\n", __FUNCTION__);
#define dbg() printf("%s %d\n", __FUNCTION__, __LINE__)
#else
#define ENTER()
#define LEAVE()
#define dbg()
#endif

#define MSG_IME_USER_SENDCHAR 8000

static key_board_t *keyboard[SFKB_NUM];
static BOOL active = TRUE;
static int HEIGHT_TASKBAR;

/*ime status table*/
static int status_table[4][4] = {
    {-1, 3, 1, -1},
    {0, 3, -1, 2},
    {0, 3, 1, -1},
    {0, -1, 1, -1}};

static int utf8_len_first_char(const char *mstr, int len)
{
    int t, c = *((const unsigned char *)(mstr++));

    if (c & 0x80)
    {
        int n = 1, ch_len = 0;
        while (c & (0x80 >> n))
            n++;

        if (n > len)
            return 0;

        ch_len = n;
        while (--n > 0)
        {
            t = *((const unsigned char *)(mstr++));

            if ((!(t & 0x80)) || (t & 0x40))
                return 0;
        }

        return ch_len;
    }

    /* for ascii character */
    return 1;
}

static void send_utf8_string(HWND target_hwnd, const char *str, int nr_ucs)
{
    ENTER();

    int len = strlen(str);
    while (len > 0)
    {
        WPARAM wParam;
        LPARAM lParam = 0;

        int ch_len = utf8_len_first_char(str, len);
        switch (ch_len)
        {
        case 1:
            wParam = (WPARAM)str[0];
            break;
        case 2:
            wParam = ((Uint8)str[0]) | (((Uint8)str[1]) << 8);
            break;
        case 3:
            wParam = ((Uint8)str[0]) | (((Uint8)str[0 + 1]) << 8) | (((Uint8)str[0 + 2]) << 16);
            break;
        case 4:
            wParam = ((Uint8)str[0]) | (((Uint8)str[0 + 1]) << 8) | (((Uint8)str[0 + 2]) << 16) | (((Uint8)str[0 + 3]) << 24);
            break;
        case 5:
            wParam = ((Uint8)str[0]) | (((Uint8)str[0 + 1]) << 8) | (((Uint8)str[0 + 2]) << 16) | (((Uint8)str[0 + 3]) << 24);
            lParam = ((Uint8)str[0 + 4]);
            break;
        case 6:
            wParam = ((Uint8)str[0]) | (((Uint8)str[0 + 1]) << 8) | (((Uint8)str[0 + 2]) << 16) | (((Uint8)str[0 + 3]) << 24);
            lParam = ((Uint8)str[0 + 4]) | (((Uint8)str[0 + 5]) << 8);
            break;
        default:
            return;
        }

#if defined(_MGRM_PROCESSES) && (MINIGUI_MAJOR_VERSION > 3)
        Send2ActiveWindow(mgTopmostLayer, MSG_CHAR, wParam, lParam);
#else
        PostMessage(target_hwnd, MSG_CHAR, wParam, lParam);
#endif

        str += ch_len;
        len -= ch_len;
    }

    LEAVE();
}

//send 2-Byte-coded chinese character to target window
static void send_ch_string(HWND target_hwnd, char *word, int len)
{
    //ENTER();
    int i;
    for (i = 0; i < len; i += 2)
    {
        WORD wDByte;
        wDByte = MAKEWORD(word[i], word[i + 1]);
#if defined(_MGRM_PROCESSES) && (MINIGUI_MAJOR_VERSION > 3)
        Send2ActiveWindow(mgTopmostLayer, MSG_CHAR, wDByte, 0);
#else
        PostMessage(target_hwnd, MSG_CHAR, wDByte, 0);
#endif
    }
    //LEAVE();
}

static void send_imeword_to_target(SOFTKBD_DATA *pdata, char *word, int len)
{
    int i = 0;
    if (len <= 0 || word == NULL)
        return;
    switch (pdata->ime_status_language)
    {
    case IME_LANGUAGE_LATIN:
        for (i = 0; i < len; i++)
        {
#if defined(_MGRM_PROCESSES) && (MINIGUI_MAJOR_VERSION > 3)
            Send2ActiveWindow(mgTopmostLayer, MSG_CHAR, word[i], 0);
#else
            PostMessage(pdata->target_hwnd, MSG_CHAR, word[i], 0);
#endif
        }
        break;
    case IME_LANGUAGE_ZHTW:
        //printf("IME_LANGUAGE_ZHTW\n");
        if (pdata->ime_status_encode == IME_ENCODING_LOCAL)
        {
            send_ch_string(pdata->target_hwnd, word, len);
        }
        else if (pdata->ime_status_encode == IME_ENCODING_UTF8)
        {
            //printf("IME_ENCODING_UTF8\n");
            send_utf8_string(pdata->target_hwnd, (char *)word, len);
        }
        break;
    case IME_LANGUAGE_ZHCN:
        //printf("IME_LANGUAGE_ZHCN\n");
        if (pdata->ime_status_encode == IME_ENCODING_LOCAL)
        {
            //printf("IME_ENCODING_LOCAL\n");
            send_ch_string(pdata->target_hwnd, word, len);
        }
        else if (pdata->ime_status_encode == IME_ENCODING_UTF8)
        {
            //printf("IME_ENCODING_UTF8\n");
            send_utf8_string(pdata->target_hwnd, word, len);
        }
        break;

    default:
        assert(0);
    }
    return;
}

static key_board_t *init_keyboard_data(HWND hWnd)
{
    /* for english key board */
    if (!(keyboard[0] = (key_board_t *)calloc(1, sizeof(key_board_t))))
    {
        _MY_PRINTF("calloc keyboard data for EN failed\n");
        return NULL;
    }
    if (-1 == init_en_keyboard(hWnd, keyboard[0]))
    {
        _MY_PRINTF("error for initalize En-keyboard.\n");
        return NULL;
    }

    /* for num key board */
    if (!(keyboard[1] = (key_board_t *)calloc(1, sizeof(key_board_t))))
    {
        _MY_PRINTF("calloc keyboard data for NUM failed\n");
        return NULL;
    }
    if (-1 == init_num_keyboard(hWnd, keyboard[1]))
    {
        _MY_PRINTF("error for initalize num-keyboard.\n");
        return NULL;
    }

    /* for punct key board */
    if (!(keyboard[2] = (key_board_t *)calloc(1, sizeof(key_board_t))))
    {
        _MY_PRINTF("calloc keyboard data for punctuation failed\n");
        return NULL;
    }
    if (-1 == init_punct_keyboard(hWnd, keyboard[2]))
    {
        _MY_PRINTF("error for initalize Puctuation-keyboard.\n");
        return NULL;
    }

    /* for rime key board */
    if (!(keyboard[3] = (key_board_t *)calloc(1, sizeof(key_board_t))))
    {
        _MY_PRINTF("calloc keyboard data for EN failed\n");
        return NULL;
    }
    if (-1 == init_rime_keyboard(hWnd, keyboard[3]))
    {
        _MY_PRINTF("error for initalize rime-keyboard.\n");
        return NULL;
    }

    //return the default keyboard at beginning.
    return keyboard[0];
}

static void destroy_key_win(void)
{
    //FIXME:
    destroy_en_keyboard(keyboard[0]);
    free(keyboard[0]);
    destroy_num_keyboard(keyboard[1]);
    free(keyboard[1]);
    destroy_punct_keyboard(keyboard[2]);
    free(keyboard[2]);
    destroy_rime_keyboard(keyboard[3]);
    free(keyboard[3]);
}

#if KBD_ANIMATE
static void on_imewnd_jmp_finished(ANIMATE_SENCE *as)
{
    SOFTKBD_DATA *pdata;
    pdata = (SOFTKBD_DATA *)as->param;
    if (!pdata->is_opened)
    {
        ShowWindow((HWND)as->normal->img, SW_HIDE);
    }
    else
    {
        ShowWindow((HWND)as->normal->img, SW_SHOW);
    }
}
#endif

static void show_ime_window(HWND hWnd, SOFTKBD_DATA *pdata, BOOL show, WPARAM wParam)
{
    if (!pdata)
        return;

    pdata->is_opened = show;
    softkey_reset();
    if (!show) //hide
    {
        if (pdata && pdata->keyboard && pdata->keyboard->clear)
            pdata->keyboard->clear(pdata->keyboard);
    }
#if KBD_ANIMATE
    int x, y;
    x = (RECTW(g_rcDesktop) - SKB_WIN_W) / 2;
    y = RECTH(g_rcDesktop) - HEIGHT_TASKBAR;
    if (show)
    {
        SetInterval(50);
        MoveWindow(hWnd, x, y - 1, SKB_WIN_W, SKB_WIN_H, FALSE);
        ShowWindow(hWnd, SW_SHOW);
        RunJumpWindow(hWnd, x, y - 1, x, y - SKB_WIN_H,
                      SKB_WIN_W, SKB_WIN_H, on_imewnd_jmp_finished, pdata);
    }
    else
    {
        if (wParam == 0)
        {
            // use animates
            RunJumpWindow(hWnd, x, y - SKB_WIN_H, x, y,
                          SKB_WIN_W, SKB_WIN_H, on_imewnd_jmp_finished, pdata);
        }
        else
        {
            pdata->is_opened = 0;
            ShowWindow(hWnd, SW_HIDE);
        }
    }
#else
    ShowWindow(hWnd, show ? SW_SHOW : SW_HIDE);
    pdata->is_opened = show;
#endif
}

static LRESULT DefaultIMEWinProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    SOFTKBD_DATA *pdata = NULL;

    if (!active)
        return 0;

    if (message != MSG_CREATE)
    {
        pdata = (SOFTKBD_DATA *)GetWindowAdditionalData(hWnd);
    }
    switch (message)
    {
    case MSG_IME_SETPOS:
        //printf("MSG_IME_SETPOS\n");
        return 1;
    case MSG_IME_SETSTATUS:
        //printf("MSG_IME_SETSTATUS\n");
        if (wParam == IME_STATUS_ENCODING)
        {
            pdata->ime_status_encode = lParam;
        }
        else if (wParam == IME_STATUS_VERSION)
        {
            return 1;
        }
        return 0;
    case MSG_IME_GETSTATUS:
        //printf("MSG_IME_GETSTATUS\n");
        if (lParam)
        {
            if (wParam == IME_STATUS_LANGUAGE)
            {
                *((int *)lParam) = pdata->ime_status_language;
            }
            else if (wParam == IME_STATUS_ENCODING)
            {
                *((int *)lParam) = pdata->ime_status_encode;
            }
        }
        return (int)pdata->is_opened;
    case MSG_IME_SETTARGET:
        //printf("MSG_IME_SETTARGET\n");
        if ((HWND)wParam != hWnd)
            pdata->target_hwnd = (HWND)wParam;
        return 0;
    case MSG_IME_SET_TARGET_INFO:
        //printf("MSG_IME_SET_TARGET_INFO\n");
        {
            IME_TARGET_INFO info = *((IME_TARGET_INFO *)lParam);
            RECT rc;
            GetWindowRect(hWnd, &rc);

#ifndef _MGI_IS_ONLY_INPUT_DEV
            if (info.iEditBoxType == IME_WINDOW_TYPE_PASSWORD)
            {
                printf("pass word edit\n");
                SendMessage(hWnd, MSG_IME_CLOSE, 0, 0);
                return 0;
            }
#endif

            if (info.ptCaret.x + RECTW(rc) > RECTW(g_rcScr))
            {
                info.ptCaret.x = RECTW(g_rcScr) - RECTW(rc);
            }
            if ((info.ptCaret.y + RECTH(rc)) > RECTH(g_rcScr))
            {
                info.ptCaret.y = info.ptCaret.y - RECTH(rc);
            }

            MoveWindow(hWnd, info.ptCaret.x, info.ptCaret.y, SKB_WIN_W, SKB_WIN_H, TRUE);
            return 0;
        }
    case MSG_IME_GETTARGET:
        return (LRESULT)pdata->target_hwnd;

    case MSG_IME_USER_SENDCHAR:
        send_imeword_to_target(pdata, pdata->keyboard->action.str, strlen(pdata->keyboard->action.str));
        break;

    case MSG_IME_OPEN:
    {
        pdata->is_closing = FALSE;
        if (pdata->is_opened)
            break;

        show_ime_window(hWnd, pdata, TRUE, wParam);

        if (GetIMEStatus(IME_STATUS_AUTOTRACK))
        {
            IME_TARGET_INFO info;
            GetIMETargetInfo(&info);
            SendMessage(hWnd, MSG_IME_SET_TARGET_INFO, 0, (LPARAM)(&info));
        }
        break;
    }

    case MSG_IME_CLOSE:
        pdata->is_closing = TRUE;
        break;

    case MSG_IDLE:
        if (pdata->is_closing)
        {
            pdata->is_closing = FALSE;
            show_ime_window(hWnd, pdata, FALSE, wParam);
        }
        break;
    }

    return DefaultMainWinProc(hWnd, message, wParam, lParam);
}

static LRESULT SoftKeyWinProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    SOFTKBD_DATA *pdata = NULL;

    if (!active)
        return 0;

    if (message != MSG_CREATE)
    {
        pdata = (SOFTKBD_DATA *)GetWindowAdditionalData(hWnd);
    }

    switch (message)
    {
    case MSG_NCCREATE:
        RegisterIMEWindow(hWnd);
        break;

    case MSG_CREATE:
    {
        if (!(pdata = (SOFTKBD_DATA *)calloc(1, sizeof(SOFTKBD_DATA))))
        {
            _MY_PRINTF("Fail to calloc SOFTKBD data.\n");
            return -1;
        }

        pdata->current_board_idx = 0;
        pdata->ime_status_encode = IME_ENCODING_UTF8;
        pdata->ime_status_language = IME_LANGUAGE_LATIN;
        if (NULL == (pdata->keyboard = init_keyboard_data(hWnd)))
        {
            _MY_PRINTF("Soft Key Window init failed\n");
            return -1;
        }

#ifdef KBD_TOOLTIP
        pdata->tooltip_win = CreateToolTip(hWnd);
#endif
        SetWindowAdditionalData(hWnd, (DWORD)pdata);
        break;
    }
    case MSG_KEYDOWN:
    case MSG_KEYUP:
#if defined(_MGRM_PROCESSES) && (MINIGUI_MAJOR_VERSION > 3)
        Send2ActiveWindow(mgTopmostLayer, message, wParam, lParam);
#else
        PostMessage(pdata->target_hwnd, message, wParam, lParam);
#endif
        return 0;

    case MSG_LBUTTONDOWN:
    case MSG_LBUTTONUP:
    case MSG_MOUSEMOVE:
    case MSG_NCMOUSEMOVE:
#ifdef KBD_TOOLTIP
        if (MSG_LBUTTONUP == message)
            HideToolTip((HWND)((SOFTKBD_DATA *)GetWindowAdditionalData(hWnd))->tooltip_win);
#endif

        if (MSG_LBUTTONDOWN == message || MSG_LBUTTONUP == message)
            reset_mouse_state(hWnd);

        switch (pdata->keyboard->proceed_msg(pdata->keyboard, hWnd, message, wParam, lParam))
        {
        case AC_CHANGE_KBD:
        {
            int y;
            POINT p;

            p.x = LOSWORD(lParam);
            p.y = HISWORD(lParam);

            md_key_t *key = pdata->keyboard->key_window->get_key(pdata->keyboard->key_window, p);
            // FIXME: If get_key return NULL, I think dispatch this message again.
            if (!key)
                break;

            /* english */
            if (key->scan_code == SCANCODE_TOEN)
            {
                y = 0;
                pdata->ime_status_language = IME_LANGUAGE_LATIN;
            }
            /* rime */
            if (key->scan_code == SCANCODE_TOPY)
            {
                y = 1;
                pdata->ime_status_language = IME_LANGUAGE_ZHCN;
            }

            /* 123 */
            if (key->scan_code == SCANCODE_TONUM)
            {
                y = 2;
                pdata->ime_status_language = IME_LANGUAGE_LATIN;
            }
            /* oprator */
            if (key->scan_code == SCANCODE_TOOP)
            {
                y = 3;
                pdata->ime_status_language = IME_LANGUAGE_LATIN;
            }

            /*clear old state */
            pdata->keyboard->clear(pdata->keyboard);
            pdata->current_board_idx = status_table[pdata->current_board_idx][y];
            pdata->keyboard = keyboard[pdata->current_board_idx];

            SendMessage(hWnd, MSG_ERASEBKGND, 0, 0L);
            softkey_reset();
            return 0;
        }
        case AC_SEND_EN_STRING:
        case AC_SEND_CN_STRING:
            SendMessage(hWnd, MSG_IME_USER_SENDCHAR, 0, 0);
            break;

        case AC_SEND_MSG:
#if defined(_MGRM_PROCESSES) && (MINIGUI_MAJOR_VERSION > 3)
            Send2ActiveWindow(mgTopmostLayer,
                              pdata->keyboard->action.message,
                              pdata->keyboard->action.wParam,
                              pdata->keyboard->action.lParam);
#else
            PostMessage(pdata->target_hwnd,
                        pdata->keyboard->action.message,
                        pdata->keyboard->action.wParam,
                        pdata->keyboard->action.lParam);
#endif
            return 0;
        }
        break;

    case MSG_PAINT:
    {
        HDC hdc = BeginPaint(hWnd);
        do
        {
            view_window_t *view_window = pdata->keyboard->view_window;
            stroke_window_t *stroke_window = pdata->keyboard->stroke_window;
            if (view_window == NULL || stroke_window == NULL)
                break;
            view_window->style |= VW_DRAW_ELMTS;
            view_window->style &= ~VW_EL_PRESSED;
            view_window->update(view_window, hWnd, NULL);
            stroke_window->update(stroke_window, hWnd);
        } while (FALSE);

        EndPaint(hWnd, hdc);
        return 0;
    }

    case MSG_ERASEBKGND:
        if (pdata && pdata->keyboard)
            pdata->keyboard->update(pdata->keyboard, hWnd, wParam, (RECT *)lParam);
        return 0;

    case MSG_CLOSE:
        UnregisterIMEWindow(hWnd);
        //SendMessage(HWND_DESKTOP, MSG_IME_UNREGISTER, (WPARAM) hWnd, 0);
        destroy_key_win();
#ifdef KBD_TOOLTIP
        DestroyMainWindow(pdata->tooltip_win);
#endif
        free(pdata);
        DestroyMainWindow(hWnd);
        PostQuitMessage(hWnd);
        return 0;
    }

    return DefaultIMEWinProc(hWnd, message, wParam, lParam);
}

static void init_createinfo(PMAINWINCREATE pCreateInfo)
{
    pCreateInfo->dwStyle = WS_ABSSCRPOS;
    pCreateInfo->dwExStyle = WS_EX_TOPMOST;
    pCreateInfo->spCaption = "Soft Key Window";
    pCreateInfo->hMenu = 0;
    pCreateInfo->hCursor = GetSystemCursor(0);
    pCreateInfo->hIcon = 0;
    pCreateInfo->MainWindowProc = SoftKeyWinProc;
    pCreateInfo->lx = (RECTW(g_rcDesktop) - SKB_WIN_W) / 2;
    pCreateInfo->ty = RECTH(g_rcDesktop) - SKB_WIN_H - HEIGHT_TASKBAR;
    pCreateInfo->rx = (RECTW(g_rcDesktop) + SKB_WIN_W) / 2;
    pCreateInfo->by = RECTH(g_rcDesktop);
    pCreateInfo->iBkColor = COLOR_lightwhite;
    pCreateInfo->dwAddData = 0;
}

static HWND create_ime_win(HWND hosting)
{
    MAINWINCREATE CreateInfo;
    HWND hMainWnd;

    init_createinfo(&CreateInfo);
    CreateInfo.hHosting = hosting;

    hMainWnd = CreateMainWindow(&CreateInfo);
    if (hMainWnd == HWND_INVALID)
        return HWND_INVALID;

    return hMainWnd;
}

void mgiEnableSoftKeypad(BOOL e)
{
    active = e;
}

#ifdef _MGRM_THREADS

typedef struct ime_info
{
    sem_t wait;
    HWND hwnd;
} IME_INFO;

static void *start_ime(void *data)
{
    MSG Msg;
    IME_INFO *ime_info = (IME_INFO *)data;
    HWND ime_hwnd;

    ime_hwnd = ime_info->hwnd = create_ime_win(HWND_DESKTOP);
    if (ime_hwnd == HWND_INVALID)
        return NULL;

    sem_post(&ime_info->wait);

    while (GetMessage(&Msg, ime_hwnd))
    {
        TranslateMessage(&Msg);
        DispatchMessage(&Msg);
    }

    MainWindowThreadCleanup(ime_hwnd);

    return NULL;
}

static pthread_t imethread;

/* the argument of 'hosting' is ignored. */
HWND skbCreateSoftKeyboard(HWND hosting, int margin_bottom)
{
    IME_INFO ime_info;
    pthread_attr_t new_attr;

    HEIGHT_TASKBAR = margin_bottom;

    sem_init(&ime_info.wait, 0, 0);
    pthread_attr_init(&new_attr);
    pthread_attr_setdetachstate(&new_attr, PTHREAD_CREATE_DETACHED);
    pthread_create(&imethread, &new_attr, start_ime, &ime_info);
    pthread_attr_destroy(&new_attr);

    sem_wait(&ime_info.wait);
    sem_destroy(&ime_info.wait);

    return ime_info.hwnd;
}

#else /* _MGRM_THREADS */

HWND skbCreateSoftKeyboard(HWND hosting, int margin_bottom)
{
    HEIGHT_TASKBAR = margin_bottom;
    return create_ime_win(hosting);
}

#endif /* !_MGRM_THREADS */
